﻿module Http

open System
open System.Net
open System.Net.Sockets
open System.IO
open UriParsing
open Utils
open HybridStreamReader
open HttpInterfaces
open Uris

let getConnection (ipAdresses : IPAddress[]) port =
    let rec getConnection1 (ipAdresses : IPAddress list) = 
        if ipAdresses.IsEmpty then failwith "No connection"
        else
            let conPort = 
                match port with
                | None -> 80
                | Some port -> port
            // Evaluate the socket and receiving host IPAddress and IPEndPoint.  

            let ip4 = ipAdresses |> List.filter (fun addr->addr.AddressFamily=AddressFamily.InterNetwork)
            if ip4.IsEmpty then failwith "No IP4 address"

            let hostAddress = ip4.Head 
            let hostEndPoint = new IPEndPoint(hostAddress, conPort);

            // Creates the Socket to send data over a TCP connection.
            let s = new System.Net.Sockets.Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp );

            // Connect to the host using its IPEndPoint.
            s.Connect(hostEndPoint);

            if s.Connected then s else getConnection1 ipAdresses.Tail

    getConnection1 (Array.toList ipAdresses)

let readFromSocket (socket:Socket) =
    let s = new NetworkStream(socket); 
    let m = new MemoryStream()
    s.CopyTo(m)
    ignore (m.Seek(0L, SeekOrigin.Begin))

    (*
    // debug: save binary stream    
    let fileStream = File.Create("C:\Users\Olav\AppData\Local\Temp\dump2.bin")
    m.Seek(0L, SeekOrigin.Begin) |> ignore
    m.CopyTo(fileStream)  |> ignore
    fileStream.Close()  |> ignore
    m.Seek(0L, SeekOrigin.Begin) |> ignore
    *)

    m

let getDataFromSocket (header:string) (socket:Socket) =
    let byteGet = System.Text.Encoding.UTF8.GetBytes(header);
    do socket.Send(byteGet, byteGet.Length, SocketFlags.None) |> ignore
    readFromSocket socket
    
let getData (request:HttpRequest) =  
    let uri = request.url

    //Set up variables and String to write to the server.
    let aSCII = System.Text.Encoding.ASCII;

    // Get DNS host information.
    if uri.host="" then failwith "missing host"
    let hostInfo = Dns.GetHostEntry(uri.host)
    // Get the DNS IP addresses associated with the host.
    let socket = getConnection hostInfo.AddressList uri.port

    let host = uri.host    
    let pathAndQuery = uri.path + uri.queryString
    let methodName =
        match request with
        | PostRequest(_,_) -> "POST"
        | GetRequest(_) -> "GET"

    let s = System.Text.StringBuilder()
             .AppendLine(methodName + " " + pathAndQuery + " HTTP/1.1")
             .AppendLine("Host: " + host)
             .AppendLine("User-Agent: FSharpBrowser")
             .AppendLine("Connection: Close")             
    match request with
    | PostRequest(_,data) ->
        do s
            .AppendLine("Content-length: " + string(data.Length))
            .AppendLine("Content-Type:application/x-www-form-urlencoded")            
            .AppendLine()
            .AppendLine(data) |> ignore
        ()
    | GetRequest(_) ->
        s.AppendLine() |> ignore
        ()
    let requestdata = s.ToString()
    getDataFromSocket requestdata socket

let rec readHeaderLines(stream : HybridStream)= 
    seq {
        let line = stream.ReadLine()
        if line <> "" then
            yield line
            yield! readHeaderLines(stream)
    } |> List.ofSeq


let parseHeader (headerline:string) =
    let segments = headerline.Split([|':'|], 2)
    segments.[0].Trim(), segments.[1].Trim() 

let parseStatus (status:string) =
    let segments = status.Split([|' '|], 3)    
    Int32.Parse(segments.[1]), segments.[2]

let readLength (stream : MemoryStream) (length:int) =   
    let buffer : byte[] = Array.zeroCreate length
    do stream.Read(buffer, 0, length) |> ignore
    new MemoryStream(buffer)

let readToEnd(stream : MemoryStream) =       
    let length = int(stream.Length - stream.Position)
    let buffer : byte[] = Array.zeroCreate length
    do stream.Read(buffer, 0, length) |> ignore
    new MemoryStream(buffer)
        
let rec readChunked(stream : HybridStream) =    
    let sizeCodeHex = stream.ReadLine()
    let size = Convert.ToInt32(sizeCodeHex, 16)     
    if size = 0 
    then []
    else
        let chunk : byte[] = Array.zeroCreate size
        let readCount = stream.Read(chunk, 0, size)
        assert (size = readCount)
        
        // read trailing line break
        stream.ReadByte() |> ignore
        stream.ReadByte() |> ignore
        //
        chunk :: readChunked(stream)    

let concat (streams:byte[] list) =
    let total = streams |> List.sumBy (fun s -> s.Length)
    let buffer = Array.zeroCreate total
    let mutable pos = 0
    for stream in streams do
        stream.CopyTo(buffer, pos)
        pos <- pos + stream.Length
    new MemoryStream(buffer)



let readText (stream:MemoryStream) (encoding:string option) =
    let encoding = match encoding with
                    | None -> Text.Encoding.UTF8
                    | Some charset -> Text.Encoding.GetEncoding(charset)
    let bodyReader = new StreamReader(stream, encoding, true)
    let txt = bodyReader.ReadToEnd()        
    TextBody(txt.ToString())

let readData mimeType (stream:MemoryStream) =
    BinaryBody(mimeType, stream)



type Response (stream : MemoryStream) =    
    // status and headers are ascii encoded
    let reader = new HybridStream(stream, Text.Encoding.ASCII)
    let status = reader.ReadLine()
    let _statusCode, statusText = parseStatus status
    let headerLines = (readHeaderLines reader) 
    // TODO: some headers can can occur multiple times, like set-cookie
    // som Map is wrong
    let _headers = headerLines |> List.map parseHeader |> Map.ofList
    let mimeType, encoding =
        match _headers.TryFind "Content-Type" with
        | Some contentType ->    
            let contentType = _headers.["Content-Type"]    
            if contentType.Contains(";") 
                then 
                    let mime, charset = partition contentType ";"
                    let key, value = partition charset "="
                    let encoding = if key="charset" then Some value else None
                    mime, encoding
                else contentType, None
        | None -> "application/octet-stream", None
    let mimeTop, mimeSub = partition mimeType "/"
    let isText = mimeTop = "text"

    let isChunked = _headers.TryFind "Transfer-Encoding" = (Some "chunked" )
    let data = 
        if isChunked then concat (readChunked reader)
        else
            let hasContentLength = _headers.ContainsKey("Content-Length")
            if hasContentLength
            then
                let contentLength = Int32.Parse(_headers.["Content-Length"]) 
                // Not implemented:
                //readContentLength(stream, contentLength)
                readLength stream contentLength
            else
                readToEnd stream
    let _body = if isText 
                then readText data encoding
                else readData mimeType data
    
    interface IHttpResponse with
        member this.statusCode = _statusCode
        member this.headers = _headers
        member this.body = _body    



let parseResponse(stream) = Response(stream) :> IHttpResponse



let getResponse (request:HttpRequest) : IHttpResponse =
    let stream = getData request
    parseResponse(stream)

let navigator (request:HttpRequest) : Async<ResponseOption> =
    let rec navigate1 (request:HttpRequest) (stack:HttpUrl list) : Async<ResponseOption> =  
        async {  
            if stack.Length>10 
            then return Failure("Caught in redirect loop")
            else 
                let response = getResponse request
                if response.statusCode=301 || response.statusCode=302 then
                    let url2 = response.headers.["Location"]
                    let url2 = parseUrl url2 <| Some request.url
                    let request2 = GetRequest(url2)
                    return! navigate1 request2 (request.url::stack)
                else return Success(response, request.url)
        }
    async {
         try
            return! navigate1 request []
         with
            ex -> return (Failure(ex.Message))
     }

